<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Read Aloud</title>

    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
    <style>
      .state {
        display: none;
        flex-flow: column;
        align-items: center;
        font-size: 1.5em;
      }
      .state label {
        font-weight: bold;
      }
      .state.pairing input {
        font-size: 2em;
      }
      .state.connected img, .state.disconnected img {
        max-width: 50vmin;
        max-height: 50vmin;
      }
    </style>

    <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.0/dist/jquery.slim.min.js" integrity="sha256-tG5mcZUtJsZvyKAxYLVXrmjKBVLd6VpVccqz/r4ypFE=" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
    <script src="https://unpkg.com/peerjs@1.4.7/dist/peerjs.min.js"></script>
    <script src="https://assets.lsdsoftware.com/lib/databind.js"></script>
    <script src="js/utils.js"></script>
    <script>
      const engine = new WebSpeechEngine()
      const query = new URLSearchParams(window.location.search)
      const peer = new Peer({debug: 2})
      const peerReady = new Promise((f,r) => peer.once("open", f).once("error", r))
      window.addEventListener("beforeunload", () => peer.destroy())

      async function connect(code) {
        await peerReady
        const conn = peer.connect(`readaloud-${code}`, {reliable: true})
        await new Promise((f,r) => conn.once("open", f).once("error", r))
        conn.on("error", console.error)
        conn.on("data", async req => {
          try {
            const result = await handleRequest(req)
            console.debug(req, result)
            conn.send({id: req.id, value: result})
          }
          catch(err) {
            console.error(err)
            conn.send({id: req.id, error: err.message})
          }
        })
        return {
          closePromise: new Promise(f => conn.once("close", f))
        }
      }

      function handleRequest(req) {
        wakeSm.trigger("wakeup")
        switch (req.method) {
          case "speak": return engine.speak(req.text, req.options)
          case "waitFinish": return engine.waitFinish(req.speechId)
          case "stop": return engine.stop()
          case "pause": return engine.pause()
          case "resume": return engine.resume()
          default: throw new Error("Bad method")
        }
      }

      function WebSpeechEngine() {
        var utter, endPromise
        this.speak = async function(text, options) {
          utter = new SpeechSynthesisUtterance()
          utter.text = text
          utter.lang = options.lang
          utter.pitch = options.pitch
          utter.rate = options.rate
          utter.volume = options.volume
          await new Promise((fulfill, reject) => {
            utter.onstart = fulfill
            utter.onerror = event => reject(new Error(event.error))
            speechSynthesis.cancel()
            speechSynthesis.speak(utter)
          })
          endPromise = new Promise((fulfill, reject) => {
            utter.onend = fulfill
            utter.onerror = event => reject(new Error(event.error))
          })
          return {speechId: 1}
        }
        this.waitFinish = async function(speechId) {
          await endPromise
        }
        this.stop = function() {
          if (utter) utter.onend = null
          speechSynthesis.cancel()
        }
        this.pause = function() {
          speechSynthesis.pause()
        }
        this.resume = function() {
          speechSynthesis.resume()
        }
      }

      const wakeSm = new StateMachine({
        IDLE: {
          wakeup() {
            return "AWAKE"
          },
          documentVisible() {}
        },
        AWAKE: {
          onTransitionIn() {
            this.promise = this._acquireWakelock()
            this.timer = this._startTimer()
          },
          async _acquireWakelock() {
            if ("wakeLock" in navigator) {
              const lock = await navigator.wakeLock.request("screen")
              console.log("WakeLock acquired")
              return lock
            }
            else {
              console.warn("WakeLock not supported")
              return {
                released: false,
                async release() {
                  this.released = true
                }
              }
            }
          },
          _startTimer() {
            return setTimeout(() => wakeSm.trigger("onTimeout"), 4*60*1000)
          },
          onTimeout() {
            this.promise
              .then(lock => lock.release())
              .then(() => console.log("WakeLock released"))
            return "IDLE"
          },
          wakeup() {
            clearTimeout(this.timer)
            this.timer = this._startTimer()
          },
          documentVisible() {
            this.promise = this.promise
              .then(lock => {
                if (lock.released == false) return lock
                lock.release().catch(console.error)
                return this._acquireWakelock()
              })
          }
        }
      })

      document.addEventListener("visibilitychange", function() {
        if (document.visibilityState == "visible") wakeSm.trigger("documentVisible")
      })
    </script>

    <script>
      state = "pairing"
      error = null

      function onSubmit(code) {
        if (code) {
          connect(code)
            .then(({closePromise}) => {
              state = "connected"
              wakeSm.trigger("wakeup")
              return closePromise
            })
            .then(() => {
              state = "disconnected"
            })
            .catch(err => {
              error = err.message || String(err)
            })
        }
        else {
          error = "Please enter pairing code"
        }
      }
    </script>
  </head>
  <body>
    <div class="position-absolute start-0 end-0 top-0 bottom-0 m-4 d-flex flex-column align-items-center justify-content-center">
      <form class="state pairing"
        bind-statement-1="$(thisElem).css('display', #state == 'pairing' ? 'flex' : 'none')"
        bind-event-submit="this.onSubmit(thisElem.code.value.trim()); return false">
        <label>Enter Pairing Code</label>
        <input class="mt-4 form-control text-center" type="number" name="code" placeholder="000-000" autocomplete="off" maxlength="6">
        <button class="d-block mt-4 btn btn-primary btn-lg" type="submit">Connect</button>
        <div class="mt-4 alert alert-danger" bind-statement-1="$(thisElem).toggle(#error != null)">{{#error}}</div>
      </form>
      <div class="state connected"
        bind-statement-1="$(thisElem).css('display', #state == 'connected' ? 'flex' : 'none')">
        <img src="images/plugged.png">
        <label class="mt-2">Connected</label>
      </div>
      <div class="state disconnected"
        bind-statement-1="$(thisElem).css('display', #state == 'disconnected' ? 'flex' : 'none')">
        <img src="images/unplugged.png">
        <label class="mt-2">Disconnected</label>
      </div>
    </div>
  </body>
</html>
